#version 460

layout(binding = 6) uniform MVP {
    mat4 model;
    mat4 view;
    mat4 proj;
    vec3 view_pos;
};

layout(location = 0) in vec3 fragPos;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texCoords;
layout(location = 3) in mat3 TBN;
layout(location = 6) in flat vec3 tangentViewPos;
layout(location = 7) in vec3 tangentFragPos;

/* Light information sent by the engine */
layout(binding = 0) buffer LightSSBO {
mat4 ssbo_Lights[];
};

/* Uniforms (Tweakable from the material editor) */
layout(location = 10) uniform vec2 u_TextureTiling = vec2(1.0, 1.0);
layout(location = 11) uniform vec2 u_TextureOffset = vec2(0.0, 0.0);
layout(location = 12) uniform vec4 u_Diffuse = vec4(1.0, 1.0, 1.0, 1.0);
layout(location = 13) uniform vec3 u_Specular = vec3(1.0, 1.0, 1.0);
layout(location = 14) uniform float u_Shininess = 100.0;
layout(location = 15) uniform float u_HeightScale = 0.0;
layout(location = 16) uniform bool u_EnableNormalMapping = false;
layout(location = 17) uniform sampler2D u_DiffuseMap;
layout(location = 18) uniform sampler2D u_SpecularMap;
layout(location = 19) uniform sampler2D u_NormalMap;
layout(location = 20) uniform sampler2D u_HeightMap;
layout(location = 21) uniform sampler2D u_MaskMap;

/* Global variables */
vec3 g_Normal;
vec2 g_TexCoords;
vec3 g_ViewDir;
vec4 g_DiffuseTexel;
vec4 g_SpecularTexel;
vec4 g_HeightTexel;
vec4 g_NormalTexel;

layout(location = 0) out vec4 FRAGMENT_COLOR;

vec3 UnPack(float p_Target) {
return vec3(float((uint(p_Target) >> 24) & 0xff) * 0.003921568627451, float((uint(p_Target) >> 16) & 0xff) * 0.003921568627451, float((uint(p_Target) >> 8) & 0xff) * 0.003921568627451);
}

bool PointInAABB(vec3 p_Point, vec3 p_AabbCenter, vec3 p_AabbHalfSize) {
return (p_Point.x > p_AabbCenter.x - p_AabbHalfSize.x && p_Point.x < p_AabbCenter.x + p_AabbHalfSize.x &&
    p_Point.y > p_AabbCenter.y - p_AabbHalfSize.y && p_Point.y < p_AabbCenter.y + p_AabbHalfSize.y &&
    p_Point.z > p_AabbCenter.z - p_AabbHalfSize.z && p_Point.z < p_AabbCenter.z + p_AabbHalfSize.z);
}

vec2 ParallaxMapping(vec3 p_ViewDir) {
const vec2 parallax = p_ViewDir.xy * u_HeightScale * texture(u_HeightMap, g_TexCoords).r;
return g_TexCoords - vec2(parallax.x, 1.0 - parallax.y);
}

vec3 BlinnPhong(vec3 p_LightDir, vec3 p_LightColor, float p_Luminosity) {
const vec3 halfwayDir = normalize(p_LightDir + g_ViewDir);
const float diffuseCoefficient = max(dot(g_Normal, p_LightDir), 0.0);
const float specularCoefficient = pow(max(dot(g_Normal, halfwayDir), 0.0), u_Shininess * 2.0);

return p_LightColor * g_DiffuseTexel.rgb * diffuseCoefficient * p_Luminosity + ((p_Luminosity > 0.0) ? (p_LightColor * g_SpecularTexel.rgb * specularCoefficient * p_Luminosity) : vec3(0.0));
}

float LuminosityFromAttenuation(mat4 p_Light) {
const vec3 lightPosition = p_Light[0].rgb;
const float constant = p_Light[0][3];
const float linear = p_Light[1][3];
const float quadratic = p_Light[2][3];

const float distanceToLight = length(lightPosition - fragPos);
const float attenuation = (constant + linear * distanceToLight + quadratic * (distanceToLight * distanceToLight));
return 1.0 / attenuation;
}

vec3 CalcPointLight(mat4 p_Light) {
    /* Extract light information from light mat4 */
const vec3 lightPosition = p_Light[0].rgb;
const vec3 lightColor = UnPack(p_Light[2][0]);
const float intensity = p_Light[3][3];

const vec3 lightDirection = normalize(lightPosition - fragPos);
const float luminosity = LuminosityFromAttenuation(p_Light);

return BlinnPhong(lightDirection, lightColor, intensity * luminosity);
}

vec3 CalcDirectionalLight(mat4 light) {
return BlinnPhong(- light[1].rgb, UnPack(light[2][0]), light[3][3]);
}

vec3 CalcSpotLight(mat4 p_Light) {
    /* Extract light information from light mat4 */
const vec3 lightPosition = p_Light[0].rgb;
const vec3 lightForward = p_Light[1].rgb;
const vec3 lightColor = UnPack(p_Light[2][0]);
const float intensity = p_Light[3][3];
const float cutOff = cos(radians(p_Light[3][1]));
const float outerCutOff = cos(radians(p_Light[3][1] + p_Light[3][2]));

const vec3 lightDirection = normalize(lightPosition - fragPos);
const float luminosity = LuminosityFromAttenuation(p_Light);

    /* Calculate the spot intensity */
const float theta = dot(lightDirection, normalize(- lightForward));
const float epsilon = cutOff - outerCutOff;
const float spotIntensity = clamp((theta - outerCutOff) / epsilon, 0.0, 1.0);

return BlinnPhong(lightDirection, lightColor, intensity * spotIntensity * luminosity);
}

vec3 CalcAmbientBoxLight(mat4 p_Light) {
const vec3 lightPosition = p_Light[0].rgb;
const vec3 lightColor = UnPack(p_Light[2][0]);
const float intensity = p_Light[3][3];
const vec3 size = vec3(p_Light[0][3], p_Light[1][3], p_Light[2][3]);

return PointInAABB(fragPos, lightPosition, size) ? g_DiffuseTexel.rgb * lightColor * intensity : vec3(0.0);
}

vec3 CalcAmbientSphereLight(mat4 p_Light) {
const vec3 lightPosition = p_Light[0].rgb;
const vec3 lightColor = UnPack(p_Light[2][0]);
const float intensity = p_Light[3][3];
const float radius = p_Light[0][3];

return distance(lightPosition, fragPos) <= radius ? g_DiffuseTexel.rgb * lightColor * intensity : vec3(0.0);
}

void main() {
g_TexCoords = u_TextureOffset + vec2(mod(texCoords.x * u_TextureTiling.x, 1), mod(texCoords.y * u_TextureTiling.y, 1));

    /* Apply parallax mapping */
if(u_HeightScale > 0) g_TexCoords = ParallaxMapping(normalize(tangentViewPos - tangentFragPos));

    /* Apply color mask */
if(texture(u_MaskMap, g_TexCoords).r != 0.0) {
g_ViewDir = normalize(view_pos - fragPos);
g_DiffuseTexel = texture(u_DiffuseMap, g_TexCoords) * u_Diffuse;
g_SpecularTexel = texture(u_SpecularMap, g_TexCoords) * vec4(u_Specular, 1.0);

if(u_EnableNormalMapping) {
g_Normal = texture(u_NormalMap, g_TexCoords).rgb;
g_Normal = normalize(g_Normal * 2.0 - 1.0);
g_Normal = normalize(TBN * g_Normal);
} else {
g_Normal = normalize(normal);
}

vec3 lightSum = vec3(0.0);

for(int i = 0;
i < ssbo_Lights.length();
++ i) {
switch(int(ssbo_Lights[i][3][0])) {
case 0 : lightSum += CalcPointLight(ssbo_Lights[i]);
break;
case 1 : lightSum += CalcDirectionalLight(ssbo_Lights[i]);
break;
case 2 : lightSum += CalcSpotLight(ssbo_Lights[i]);
break;
case 3 : lightSum += CalcAmbientBoxLight(ssbo_Lights[i]);
break;
case 4 : lightSum += CalcAmbientSphereLight(ssbo_Lights[i]);
break;
}
}

FRAGMENT_COLOR = vec4(lightSum, g_DiffuseTexel.a);
} else {
FRAGMENT_COLOR = vec4(0.0);
}
}